package com.xhj.mybatis.builder.xml;

import com.xhj.mybatis.builder.BaseBuilder;
import com.xhj.mybatis.builder.MapperBuilderAssistant;
import com.xhj.mybatis.builder.ResultMapResolver;
import com.xhj.mybatis.cache.Cache;
import com.xhj.mybatis.io.Resources;
import com.xhj.mybatis.mapping.ResultFlag;
import com.xhj.mybatis.mapping.ResultMap;
import com.xhj.mybatis.mapping.ResultMapping;
import com.xhj.mybatis.reflection.MetaClass;
import com.xhj.mybatis.session.Configuration;
import com.xhj.mybatis.type.JdbcType;
import com.xhj.mybatis.type.TypeHandler;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

/**
 * XMLMapperBuilder
 *
 * @author XJks
 * @description XML映射器构建器
 */
public class XMLMapperBuilder extends BaseBuilder {

    /**
     * XxxMapper.xml 文件的资源路径
     */
    private String resource;

    /**
     * 解析 XxxMapper.xml 文件时，对应的根节点 <mapper> 元素
     */
    private Element rootElement;

    /**
     * 当前命名空间
     */
    private String currentNamespace;

    /**
     * Mapper 构建助手，辅助构建和注册各种映射配置（如 ResultMap，MappedStatement 等）
     */
    private final MapperBuilderAssistant builderAssistant;

    public XMLMapperBuilder(Configuration configuration, String resource, InputStream inputStream) throws DocumentException {
        this(configuration, resource, new SAXReader().read(inputStream));
    }

    private XMLMapperBuilder(Configuration configuration, String resource, Document document) {
        super(configuration);
        this.resource = resource;
        this.rootElement = document.getRootElement();
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    }

    /**
     * 解析 mapper 映射器文件
     */
    public void parse() {
        // 如果当前资源没有加载过再加载，防止重复加载
        if (!configuration.isResourceLoaded(resource)) {
            // 解析 <mapper> 根节点
            configurationElement(rootElement);
            // 标记该资源已加载
            configuration.addLoadedResource(resource);
            // 为当前命名空间绑定对应的 Mapper 类
            bindMapperForNamespace();
        }
    }

    /**
     * 解析 <mapper> 根节点，构建映射器相关配置。
     *
     * @param context 根节点 <mapper> 元素
     */
    private void configurationElement(Element context) {
        try {
            // 读取 namespace 属性，标识当前 Mapper 的命名空间（通常是接口全限定名）
            currentNamespace = context.attributeValue("namespace");
            // 如果 namespace 为空，抛出异常，Mapper 必须有命名空间
            if (currentNamespace == null || currentNamespace.isEmpty()) {
                throw new RuntimeException("Mapper's namespace cannot be empty");
            }
            // 设置当前命名空间，后续所有配置都归属于该命名空间
            builderAssistant.setCurrentNamespace(currentNamespace);
            // TODO:解析 <cache> 节点，处理二级缓存配置
            cacheElement(context.element("cache"));
            // TODO:解析所有 <resultMap> 节点，处理结果映射配置
            resultMapElements(context.elements("resultMap"));
            // TODO:解析所有 SQL 语句节点（select、insert、update、delete），构建对应的 Statement
            // 声明一个集合用于存放需要解析的节点
            List<Element> elementList = new ArrayList<>();
            // 收集 <select>，<insert>，<update>，<delete> 节点
            elementList.addAll(context.elements("select"));
            elementList.addAll(context.elements("insert"));
            elementList.addAll(context.elements("update"));
            elementList.addAll(context.elements("delete"));
            // 解析 <select>, <insert>, <update>, <delete> 节点
            buildStatementFromContext(elementList);
        } catch (Exception e) {
            // 捕获异常并抛出自定义异常，包含资源信息，便于定位问题
            throw new RuntimeException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }

    /**
     * 构建 SQL 语句节点，构建 MappedStatement 对象
     *
     * @param list 节点列表
     */
    private void buildStatementFromContext(List<Element> list) {
        // 如果配置中指定了 databaseId（数据库标识），优先解析与当前数据库匹配的语句
        if (configuration.getDatabaseId() != null) {
            /**
             * TODO:
             *  1.因为执行 {@link #buildStatementFromContext(List<Element> list, String requiredDatabaseId)} 方法时传入了不为 null 的 databaseId
             *  2.所以该方法会优先解析那些带有 databaseId 属性且值与配置中 databaseId 匹配的 SQL 语句节点。
             *      2.1 如果某个 SQL 语句节点的 databaseId 属性与配置中的 databaseId 相同，则会被解析并构建对应的 MappedStatement。
             *          之后在执行 {@code buildStatementFromContext(list, null)} 语句时，这些已经解析过的语句节点会被跳过，避免重复解析，所以 {@link Configuration#mappedStatements} 中不会有重复的 MappedStatement，要么对应的 mappedStatements 中只有带 databaseId 的语句，要么只有不带 databaseId 的语句。
             *      2.2 如果 XML 文件中没有对应 databaseId 的语句节点，则执行完 {@code buildStatementFromContext(list, configuration.getDatabaseId())} 语句后，{@link Configuration#mappedStatements} 大小为 0。
             *          在后续调用 {@code buildStatementFromContext(list, null)} 时解析那些不带 databaseId 属性的通用语句节点，{@link Configuration#mappedStatements} 就会包含这些通用语句的 MappedStatement。
             */
            buildStatementFromContext(list, configuration.getDatabaseId());
        }
        // 再解析没有 databaseId 限定的语句（通用语句）
        buildStatementFromContext(list, null);
    }

    /**
     * 实际解析 <select>, <insert>, <update>, <delete> 这些 SQL 语句节点的方法，支持按数据库标识过滤，构建 MappedStatement 对象。
     * TODO:
     *  1.该方法的主要是解析 XML 文件中的 SQL 语句节点，而不是基于注解解析 Mapper 接口的方法构建 MappedStatement。
     *  2.解析 XML 文件中的 SQL 语句节点构建 MappedStatement，是本类 {@link XMLMapperBuilder} 的功能。
     *  3.基于注解解析 Mapper 接口的方法构建 MappedStatement，是 {@link com.xhj.mybatis.builder.annotation.MapperAnnotationBuilder} 的功能。
     *
     * @param list               节点列表
     * @param requiredDatabaseId 数据库标识符
     */
    private void buildStatementFromContext(List<Element> list, String requiredDatabaseId) {
        // 遍历所有 SQL 语句节点
        for (Element context : list) {
            // 创建 XMLStatementBuilder，用于解析单个 SQL 语句节点
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
            // 尝试解析 SQL 语句节点，生成对应的 Statement 映射
            statementParser.parseStatementNode();
        }
    }

    /**
     * 解析 <resultMap> 节点列表，依次构建每个 ResultMap 对象。
     * ResultMap 用于描述数据库结果集与 Java 对象属性的映射关系。
     * 该方法会遍历传入的所有 <resultMap> 节点，并尝试解析它们。
     * 如果某个 <resultMap> 依赖的类型或父映射还未加载，会抛出 IncompleteElementException，
     * 此时会被捕获并忽略，等依赖加载后会再次重试解析。
     *
     * @param list 包含所有 <resultMap> 节点的列表
     */
    private void resultMapElements(List<Element> list) {
        // 遍历每一个 <resultMap> 节点
        for (Element resultMapNode : list) {
            // 解析当前 <resultMap> 节点，构建 ResultMap 对象
            resultMapElement(resultMapNode);
        }
    }

    /**
     * 解析 <cache> 节点，配置二级缓存
     *
     * @param context 当前 <cache> 节点
     */
    private void cacheElement(Element context) {
        // 如果 context 不为 null，说明 XML 中存在 <cache> 节点
        if (context != null) {
            // 获取 type 属性，指定缓存实现类型，默认为 PERPETUAL
            String type = context.attributeValue("type", "PERPETUAL");
            // 通过类型别名注册器解析 type，获取对应的 Cache 实现类
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            // 获取 eviction 属性，指定缓存的回收策略，默认为 LRU
            String eviction = getStringAttributeOfDom4jElement(context, "eviction", "LRU");
            // 解析 eviction，获取对应的 Cache 实现类
            Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
            // 获取 flushInterval 属性，指定缓存刷新间隔（毫秒），可选
            Long flushInterval = getLongAttributeOfDom4jElement(context, "flushInterval");
            // 获取 size 属性，指定缓存的最大对象数，超出后按照回收策略清理
            Integer size = getIntegerAttributeOfDom4jElement(context, "size");
            // 获取 readOnly 属性，指定缓存是否只读，默认为 false，取反后表示是否可读写
            boolean readWrite = !getBooleanAttributeOfDom4jElement(context, "readOnly", false);
            // 获取 blocking 属性，指定缓存是否阻塞，默认为 false
            boolean blocking = getBooleanAttributeOfDom4jElement(context, "blocking", false);
            // 获取 <cache> 节点下的所有子节点属性，封装为 Properties
            Properties props = getChildrenAsProperties(context);
            // 调用 builderAssistant，创建并使用新的缓存配置
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }

    /**
     * 解析 <resultMap> 节点，构建 ResultMap 对象（用于描述数据库结果集与 Java 对象属性的映射关系）。
     * 这是一个重载方法，简化调用，只需要传入 resultMapNode 节点即可。
     * 实际调用了带有更多参数的重载方法，默认不传递额外的结果映射和外部类型信息。
     *
     * @param resultMapNode 当前 <resultMap> 节点
     * @return 解析得到的 ResultMap 对象
     */
    private ResultMap resultMapElement(Element resultMapNode) {
        // 调用重载方法，传入空的结果映射列表和 null（没有外部包裹类型）
        return resultMapElement(resultMapNode, Collections.emptyList(), null);
    }

    /**
     * 解析 <resultMap> 节点，构建 ResultMap 对象
     *
     * @param resultMapNode            当前 <resultMap> 节点
     * @param additionalResultMappings 额外的结果映射（如嵌套映射时传递）
     * @param enclosingType            外部包裹类型（用于推断类型）
     * @return 解析得到的 ResultMap 对象
     */
    private ResultMap resultMapElement(Element resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
        // 依次尝试获取 type、ofType、resultType、javaType 属性，确定映射的 Java 类型
        String type = getStringAttributeOfDom4jElement(resultMapNode, "type", "ofType", "resultType", "javaType");
        // 将类型字符串解析为 Class 对象
        Class<?> typeClass = resolveClass(type);
        // 如果未能解析出类型，则尝试从外部类型推断
        if (typeClass == null) {
            typeClass = inheritEnclosingType(resultMapNode, enclosingType);
        }
        // 结果映射集合，先添加额外传入的映射
        List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
        // 获取 <resultMap> 下的所有子节点（如 id、result、association、collection 等）
        List<Element> resultChildren = resultMapNode.elements();
        // 遍历所有子节点，逐一处理
        for (Element resultChild : resultChildren) {
            // todo:源码中还存在处理 <discriminator> 节点的逻辑，这里暂不实现。
            // 如果是 <constructor> 节点，处理构造方法参数映射
            if ("constructor".equals(resultChild.getName())) {
                processConstructorElement(resultChild, typeClass, resultMappings);
            } else {
                // 其他节点（如 id、result、association、collection 等）
                // 标记当前映射的标志集合
                List<ResultFlag> flags = new ArrayList<>();
                // 如果是 <id> 节点，添加 ID 标志
                if ("id".equals(resultChild.getName())) {
                    flags.add(ResultFlag.ID);
                }
                // 构建 ResultMapping 对象并添加到集合
                resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
            }
        }
        // 获取 <resultMap> 的 id 属性
        String id = resultMapNode.attributeValue("id");
        // 获取 <resultMap> 的 autoMapping 属性（是否自动映射）
        Boolean autoMapping = getBooleanAttributeOfDom4jElement(resultMapNode, "autoMapping");
        // 创建 ResultMapResolver，用于最终解析 ResultMap
        ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, resultMappings, autoMapping);
        // 解析并返回 ResultMap
        return resultMapResolver.resolve();
    }

    /**
     * 尝试根据外部类型（enclosingType）推断当前节点（如 association、case）的 Java 类型。
     * 主要用于 <resultMap> 嵌套映射时，如果没有显式指定 type 属性，则通过父类型和 property 属性来推断。
     *
     * @param resultMapNode 当前 <resultMap> 的子节点（如 association、case）
     * @param enclosingType 外部包裹类型（即父 <resultMap> 的 Java 类型）
     * @return 推断得到的 Java 类型 Class，如果无法推断则返回 null
     */
    protected Class<?> inheritEnclosingType(Element resultMapNode, Class<?> enclosingType) {
        // 如果当前节点是 <association>，并且没有指定 resultMap 属性
        if ("association".equals(resultMapNode.getName()) && resultMapNode.attributeValue("resultMap") == null) {
            // 获取 property 属性，表示关联的属性名
            String property = resultMapNode.attributeValue("property");
            // 如果 property 不为空，且外部类型不为空
            if (property != null && enclosingType != null) {
                // 通过 MetaClass 获取外部类型的元信息
                MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
                // 获取该属性的 setter 方法对应的参数类型（即属性类型）
                return metaResultType.getSetterType(property);
            }
            // 如果当前节点是 <case>，并且没有指定 resultMap 属性
        } else if ("case".equals(resultMapNode.getName()) && resultMapNode.attributeValue("resultMap") == null) {
            // 直接返回外部类型
            return enclosingType;
        }
        // 其他情况无法推断，返回 null
        return null;
    }

    /**
     * 处理 <constructor> 节点，将其子节点（如 <arg>、<idArg>）解析为构造方法参数的映射关系。
     *
     * @param resultChild    <constructor> 节点
     * @param resultType     结果类型的 Class
     * @param resultMappings 结果映射集合，用于收集解析结果
     */
    private void processConstructorElement(Element resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
        // 获取 <constructor> 节点下的所有子节点（如 <arg>、<idArg>），每个子节点代表一个构造方法参数
        List<Element> argChildren = resultChild.elements();
        // 遍历每一个参数子节点
        for (Element argChild : argChildren) {
            // 创建一个用于标记参数类型的集合
            List<ResultFlag> flags = new ArrayList<>();
            // 所有构造方法参数都加上 CONSTRUCTOR 标志
            flags.add(ResultFlag.CONSTRUCTOR);
            // 如果当前参数是 <idArg>，则再加上 ID 标志，表示主键参数
            if ("idArg".equals(argChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            // 构建参数的 ResultMapping 对象，并添加到结果映射集合中
            resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
        }
    }

    /**
     * 根据 XML 节点和相关参数，构建 ResultMapping 对象（结果映射）。
     * ResultMapping 用于描述数据库字段与 Java 对象属性之间的映射关系。
     *
     * @param context    当前的 XML 节点（如 <id>、<result>、<association> 等）
     * @param resultType 结果类型的 Class（即映射到的 Java 类）
     * @param flags      标志集合，标识当前映射的特殊属性（如是否为构造方法参数、主键等）
     * @return 构建好的 ResultMapping 对象
     */
    private ResultMapping buildResultMappingFromContext(Element context, Class<?> resultType, List<ResultFlag> flags) {
        // 定义属性名变量
        String property;
        /**
         * TODO:
         *  1.由于是在 {@code <constructor>} 中使用，则里面的标签类型只能是 {@code <idArg>} 或 {@code <arg>}
         *  2.这些 {@code <idArg>} 或 {@code <arg>} 标签在字段映射时，所用的属性名称必须使用 'name' 属性来指定，不能使用 'property' 属性
         *  3.由于目前没有实现嵌套映射功能，不能使用嵌套的 resultMap 来映射关联对象的属性，所以这里没有解析 <association> 或 <collection> 标签
         */
        if (flags.contains(ResultFlag.CONSTRUCTOR)) {
            property = context.attributeValue("name");
        } else {
            // 否则用 property 属性（普通字段映射）
            property = context.attributeValue("property");
        }
        // 获取 column 属性，表示数据库字段名
        String column = context.attributeValue("column");
        // 获取 javaType 属性，表示 Java 类型（如 java.lang.String）
        String javaType = context.attributeValue("javaType");
        // 获取 jdbcType 属性，表示 JDBC 类型（如 VARCHAR、INTEGER）
        String jdbcType = context.attributeValue("jdbcType");
        // todo:源码这里还有处理嵌套 resultMap 的逻辑，这里暂不实现。
        // 获取 notNullColumn 属性，指定哪些列不能为空
        String notNullColumn = context.attributeValue("notNullColumn");
        // 获取 columnPrefix 属性，指定列名前缀
        String columnPrefix = context.attributeValue("columnPrefix");
        // 获取 typeHandler 属性，指定类型处理器
        String typeHandler = context.attributeValue("typeHandler");
        // 将 javaType 字符串解析为 Class 对象
        Class<?> javaTypeClass = resolveClass(javaType);
        // 将 typeHandler 字符串解析为类型处理器的 Class
        Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
        // 将 jdbcType 字符串解析为 JdbcType 枚举
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        // 调用 builderAssistant 构建 ResultMapping 对象，封装所有配置信息
        return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, notNullColumn, columnPrefix, typeHandlerClass, flags);
    }

    /**
     * TODO:绑定当前命名空间对应的 Mapper 类，将其注册到全局配置的 MapperRegistry 中。
     */
    private void bindMapperForNamespace() {
        // 获取当前的命名空间（一般是 Mapper 接口的全限定类名）
        String namespace = builderAssistant.getCurrentNamespace();
        // 如果命名空间不为空，才进行绑定
        if (namespace != null) {
            // 用于保存要绑定的类型
            Class<?> boundType = null;
            try {
                // 尝试通过类名加载对应的 Class 对象
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException e) {
                // 如果找不到类，说明没有对应的 Mapper 接口，忽略即可
            }
            // 如果成功加载了类，并且全局配置中还没有注册该 Mapper
            if (boundType != null && !configuration.hasMapper(boundType)) {
                /**
                 * todo:
                 *  源码说明:
                 *      1.Spring 可能不知道真正的资源名，所以这里加一个标记，防止后续重复加载
                 *      2.具体可以参考 MapperAnnotationBuilder#loadXmlResource 方法
                 */
                configuration.addLoadedResource("namespace:" + namespace);
                // 将该 Mapper 类注册到全局配置中
                configuration.addMapper(boundType);
            }
        }
    }
}
